﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Net;
using System.Runtime.InteropServices;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;


namespace NetworkInterfaceUtility
{      

    public enum ClientConnectedEvent
    {
        CLIENT_CONNECTED,
        LOCAL_ID_SPECIFIER,
        CLIENT_DISCONNECTED,
    }


    public class NIU
    {
        public static bool verboseDebugMessages = false;
        public static bool packetDebugMessages = false;

        public static TcpListener tcpListener;
        public static Thread listenThread;

        private static NetworkClient host = new NetworkClient();
        private static NetworkClient[] clientList = new NetworkClient[256];
        int clientCount = 0;

        public int ClientCount
        {
            get { return clientCount; }
            private set { clientCount = value; }
        }

        public delegate object NIUMessageDispatcher(Delegate method, params object[] args);

        public delegate void LogMessageDelegate(string message);
        public event LogMessageDelegate StatusLogEvent = delegate { };
        public delegate void NIUCallBackMethod(NetworkMessageData message);
        public delegate void NIUClientConnectedCallbackMethod(Int32 clientID, ClientConnectedEvent eventType);
        private static Dictionary<Int32, NetworkMessageDispatcher> NIUMessageDispatchers = new Dictionary<int,NetworkMessageDispatcher>();
        private static ClientConnectedDispatcher clientConnectedDispatcher = null;
        private Int32 localClientID = -1;
        public Int32 LocalClientID
        {
            get 
            {
                if(!localClientIDValid)
                    return -2;
                return localClientID;
            }
        }
        private bool localClientIDValid = false;
        public bool LocalClientIDValid
        {
            get { return localClientIDValid; }
        }

        private class NetworkMessageDispatcher
        {
            public NetworkMessageDispatcher(NIUCallBackMethod callback, NIUMessageDispatcher dispatcher)
            {
                this.callback = callback;
                this.dispatcher = dispatcher;
            }
            public NIUCallBackMethod callback = null;
            public NIUMessageDispatcher dispatcher = null;

            public void Dispatch(NetworkMessageData message)
            {
                if (dispatcher != null)
                    dispatcher(callback, message);
                else
                    callback(message);
            }
        }
        private class ClientConnectedDispatcher
        {
            public ClientConnectedDispatcher(NIUClientConnectedCallbackMethod callback, NIUMessageDispatcher dispatcher)
            {
                this.callback = callback;
                this.dispatcher = dispatcher;
            }
            public NIUClientConnectedCallbackMethod callback = null;
            public NIUMessageDispatcher dispatcher = null;

            public void Dispatch(Int32 clientID, ClientConnectedEvent connected)
            {
                if (dispatcher != null)
                    dispatcher(callback, clientID, connected);
                else
                    callback(clientID, connected);
            }
        }

        internal class NetworkClient
        {
            public NetworkClient()
            {     
                processingState = NetworkDataPocessingState.PROCESSING_HEADER;
                stream = null;
                fragmentedMessage = null;
                fragmentedMessageIndex = 0;
                fragmentedMessageIndex = 0;
                assembledHeader = 0;
            }
            internal NetworkDataPocessingState processingState = NetworkDataPocessingState.PROCESSING_HEADER;
            internal NetworkStream stream;
            internal byte[] fragmentedMessage;
            internal int fragmentedMessageIndex = 0;
            internal readonly byte[] fragmentedHeader = new byte[4];
            internal int fragmentedHeaderIndex = 0;
            internal Int32 assembledHeader;
        }

        internal enum NetworkDataPocessingState
        {
            PROCESSING_HEADER,
            PROCESSING_MESSAGE,
        }

        public void RegisterNetworkMessageHandler(Int32 messageID, NIUCallBackMethod callback, NIUMessageDispatcher threadsafeDispatcher)
        {
            //for now, require one to one mapping of message ids to callbacks
            NIUMessageDispatchers.Add(messageID, new NetworkMessageDispatcher(callback, threadsafeDispatcher));
        }  
        public void RegisterNetworkMessageHandler(Int32 messageID, NIUCallBackMethod callback)
        {
            //for now, require one to one mapping of message ids to callbacks
            NIUMessageDispatchers.Add(messageID, new NetworkMessageDispatcher(callback, null));
        }

        public void UnregisterNetworMessageHandler(Int32 messageID)
        {
            NIUMessageDispatchers.Remove(messageID);
        }

        public void RegisterClientConnectedHandler(NIUClientConnectedCallbackMethod callback, NIUMessageDispatcher threadsafeDispatcher)
        {
            //for now, allow only one client connected handler
            clientConnectedDispatcher = new ClientConnectedDispatcher(callback, threadsafeDispatcher);
        }
        public void RegisterClientConnectedHandler(NIUClientConnectedCallbackMethod callback)
        {
            //for now, allow only one client connected handler
            clientConnectedDispatcher = new ClientConnectedDispatcher(callback, null);
        }

        private bool connectedToHost = false;
        public bool ConnectedToHost
        {
            get { return connectedToHost; }
            set { } //readonly field
        }

        private int maxClientCount = 0;

        public int MaxClientCount
        {
            get { return maxClientCount; }
            set 
            {  maxClientCount = value; }
        }

        private bool hosting = false;
        public bool Hosting
        {
            get { return hosting; }
            set { } //readonly field
        }

        private void Log(String logMessage)
        {
            StatusLogEvent.Invoke(logMessage);
        }


        public NIU(LogMessageDelegate logMessageDelegate = null)
        {
            if (logMessageDelegate != null)
                this.StatusLogEvent += logMessageDelegate;
        }

        public void Host(int maxClientCount)
        {   
            if (hosting)
            {
                Log("Attempted to call Host while already hosting. StopHosting must be called first. Call ignored.");
                return;
            }

            clientList = new NetworkClient[clientList.Length];

            localClientID = -1;
            localClientIDValid = true;
            clientCount = 0;

            Log(String.Format("Initializing host socket."));
            this.maxClientCount = maxClientCount;


            listenThread = new Thread(new ThreadStart(ListenForClients));
            listenThread.Start();

            hosting = true;
        }

        private void ListenForClients()
        {
            Log(String.Format("Listening for clients."));

            bool listening = true;

            /*
            // Get host name
            String strHostName = Dns.GetHostName();
            Console.WriteLine("Host Name: " + strHostName);

            // Find host by name
            IPHostEntry iphostentry = Dns.GetHostByName(strHostName);*/

            try
            {          
                tcpListener = new TcpListener(IPAddress.Any, 25654);
                tcpListener.Start();

                while (listening)
                {
                    while (clientCount < maxClientCount && listening)
                    {
                        //blocks until a client has connected to the server
                        TcpClient client = tcpListener.AcceptTcpClient();

                        if (client == null)
                        {    
                            listening = false;
                            break;
                        }

                        //create a thread to handle communication 
                        //with connected client
                        Thread clientThread = new Thread(new ParameterizedThreadStart(NetworkMessageHandlerLoop));
                        clientThread.Start(client);
                    }
                    while (clientCount >= maxClientCount && listening)
                    {
                        Thread.Sleep(10);
                    }
                }
            }
            catch(SocketException e)
            {
                Log(String.Format("ListenForClients thread threw an exception: {0}.",e.Message));
            }

            //Log(String.Format("Stopped listening for clients."));
        }

        static private int maximumMessageSize = 16384;

        private void NetworkMessageHandlerLoop(object tcpClientObject)
        {
            TcpClient threadTcpClient = (TcpClient)tcpClientObject;
            NetworkStream threadClientStream = threadTcpClient.GetStream();
            NetworkClient threadClient = null;
            Int32 threadClientID = 0;

            if (hosting)
            {
                threadClient = new NetworkClient();

                for (threadClientID = 0; threadClientID < maxClientCount; threadClientID++)
                {
                    if (clientList[threadClientID] == null)
                    {
                        //Monitor.Enter(clientList);
                        clientList[threadClientID] = threadClient;
                        clientList[threadClientID].stream = threadClientStream;
                        //Monitor.Exit(clientList);
                        clientCount++;

                        break;
                    }
                }
     
                if (verboseDebugMessages)
                {
                    Log(String.Format("Sending message of size: {0}", 5));
                }
                //send the client's host-based ID to the client
                char clientIDMessage = (char)threadClientID;
                Int32 messageSize = 1;
                byte[] messageData = new byte[5];
                Array.Copy(BitConverter.GetBytes(messageSize), 0, messageData, 0, 4);
                messageData[4] = (byte)clientIDMessage;
                threadClientStream.Write(messageData, 0, 5);    
 
                
                if (clientConnectedDispatcher != null)
                    clientConnectedDispatcher.Dispatch(threadClientID, ClientConnectedEvent.CLIENT_CONNECTED);
                Log(String.Format("Client: {0} has connected to the server.", threadTcpClient.Client.RemoteEndPoint.ToString()));
            }
            if (connectedToHost)
            {
                threadClient = host;
                Log(String.Format("Successfully connected to server: {0}", threadTcpClient.Client.RemoteEndPoint.ToString()));
            }


            byte[] message = new byte[maximumMessageSize];
            int bytesRead;


            while (true)
            {
                bytesRead = 0;

                try
                {
                    //blocks until a client sends a message
                    bytesRead = threadClientStream.Read(message, 0, maximumMessageSize);
                }
                catch (SocketException e)
                {
                    //a socket error has occured
                    Log(String.Format("HandleClientComm threw exception: {0} while listening for client: {1} ", e.Message, threadClientID));
                    break;
                }
                catch (ObjectDisposedException e)
                {
                    Log(String.Format("HandleClientComm threw exception: {0} while listening for client: {1} ", e.Message, threadClientID));
                    break;
                }
                catch (IOException e)
                {
                    Log(String.Format("HandleClientComm threw exception: {0} while listening for client: {1} ", e.Message, threadClientID));
                    break;
                }

                if (verboseDebugMessages)
                {
                    Log("Received message of size: " + bytesRead.ToString());
                }
                if (bytesRead == 0)
                {
                    break;
                }

                ReadNetworkDataStream(message, bytesRead, 0, threadClient, threadClientID);
                //HandleNetworkMessage(DeSerialize(message), clientStream, clientID);

            }

            //the client has disconnected from the server
            try
            {
                if (hosting)
                {
                    for (int i = 0; i < maxClientCount; i++)
                    {
                        if (clientList[i] != null)
                        {
                            if (clientList[i].stream == threadClientStream)
                            {
                                //Monitor.Enter(clientList);
                                clientList[i] = null;
                                //Monitor.Exit(clientList);
                                clientCount--;
                            }
                        }
                    }

                    if (clientConnectedDispatcher != null)
                        clientConnectedDispatcher.Dispatch(threadClientID, ClientConnectedEvent.CLIENT_DISCONNECTED);
                    Log(String.Format("Client: {0} has disconnected from the server.", threadTcpClient.Client.RemoteEndPoint.ToString()));
                }
                if (connectedToHost)
                {
                    connectedToHost = false;

                    localClientIDValid = false;
                    localClientID = -1;

                    if (clientConnectedDispatcher != null)
                        clientConnectedDispatcher.Dispatch(threadClientID, ClientConnectedEvent.CLIENT_DISCONNECTED);

                    Log(String.Format("Connection to {0} has been terminated.", threadTcpClient.Client.RemoteEndPoint.ToString()));
                }

                threadClientStream.Close();
            }
            catch (ObjectDisposedException e)
            {
                Log(String.Format("Error attempting to close socket. {0}", e.Message));
            }
            catch (InvalidOperationException e)
            {

            }
            catch (SocketException e)
            {
                Log(String.Format("Error attempting to close socket. {0}", e.Message));
            }
        }

        public void BroadcastNetworkMessage(NetworkMessageData networkMessageData)
        {
            if (!hosting && !connectedToHost)
            {
                return;
            }

            //debugging ONLY:
            //return;

            networkMessageData.broadcastMessage = true;
            networkMessageData.sourceClientID = localClientID;
            if (hosting)
            {
                foreach (NetworkClient client in clientList)
                {
                    if (client != null)
                    {
   
                        //byte[] data = Serialize(networkMessageData);  
                        //if (excessiveDebugMessage)
                        //{
                        //    Log("Sending message of size: " + data.Length.ToString());
                        //}
                        //TBD: may need blocking here
                       // Monitor.Enter(stream);
                        //stream.Write(data, 0, data.Length);
                        WriteNetworkMessage(networkMessageData, client.stream);
                       // Monitor.Exit(stream);
                    }
                }
            }
            if (connectedToHost)
            {  
                
                //byte[] data = Serialize(networkMessageData);

                //TBD: may need blocking here
                //Monitor.Enter(hostSocket);
                WriteNetworkMessage(networkMessageData, host.stream);
                //  hostStream.Write(data, 0, data.Length);
               // Monitor.Exit(hostSocket);
            }
        }

        public void SendNetworkMessage(NetworkMessageData networkMessageData, Int32 destinationClientID)
        {
            if (hosting)
            {
                WriteNetworkMessage(networkMessageData, clientList[destinationClientID].stream);
            }
            else //for now, clients can only send to host (and specify broadcast)
            {
                WriteNetworkMessage(networkMessageData, host.stream);
            }
        }      

        public void SendNetworkMessage(NetworkMessageData networkMessageData)
        {
            if (hosting)
            {
                Log("Call to SendNetworkMessage while hosting with no destination ID. Consider using broadcastNetworkMessage.");
            }
            else //for now, clients can only send to host (and specify broadcast)
            {
                WriteNetworkMessage(networkMessageData, host.stream);
            }
        }   

        private void WriteNetworkMessage(NetworkMessageData networkMessageData, NetworkStream destination)
        {
            //serialize the network data message structure
            byte[] objectData = Serialize(networkMessageData);
            Int32 size = objectData.Length;
            byte[] message = new byte[size + 4];
            //prepend the size of the message      
            if (verboseDebugMessages)
            {
                Log("Sending message of size: " + message.Length.ToString());
            }
            Array.Copy(BitConverter.GetBytes(size), 0, message, 0, 4);
            Array.Copy(objectData, 0, message, 4, size);
            if (packetDebugMessages)
            {
                String logMessage =  Environment.NewLine + "Sending datastream of size " + message.Length.ToString() + Environment.NewLine;
                for (int i = 0; i < message.Length; i++)
                {
                    logMessage += message[i].ToString() + " ";
                }
                logMessage += Environment.NewLine;
                Log(logMessage);
            }
            destination.Write(message,0,message.Length);
        }

        private void ReadNetworkDataStream(byte[] data, Int32 dataSize,Int32 dataIndex,NetworkClient client, Int32 clientID)
        {
            Int32 headerBytesRead = 0;
            Int32 messageBytesRead = 0;

            switch (client.processingState)
            {
                case NetworkDataPocessingState.PROCESSING_HEADER:
                    headerBytesRead = Math.Min(4 - client.fragmentedHeaderIndex, dataSize-dataIndex); 
                    Array.Copy(data, dataIndex, client.fragmentedHeader, client.fragmentedHeaderIndex, headerBytesRead);
                    client.fragmentedHeaderIndex += headerBytesRead;
                    dataIndex += headerBytesRead;

                    if (client.fragmentedHeaderIndex == 4)
                    {
                        client.assembledHeader = BitConverter.ToInt32(client.fragmentedHeader,0);          
                        client.fragmentedHeaderIndex = 0;
                        client.fragmentedMessage = new byte[client.assembledHeader];
                        client.processingState = NetworkDataPocessingState.PROCESSING_MESSAGE;
                    }
                    break;

                case NetworkDataPocessingState.PROCESSING_MESSAGE:
                    messageBytesRead = Math.Min(client.assembledHeader, dataSize - dataIndex);
                    Array.Copy(data, dataIndex, client.fragmentedMessage, client.fragmentedMessageIndex, messageBytesRead);
                    client.fragmentedMessageIndex += messageBytesRead;
                    dataIndex += messageBytesRead;

                    //if the client's message fragment index is equal to the fragment size..
                    if (client.fragmentedMessageIndex == client.assembledHeader)
                    {
                        //..we've read a whole message

                        // the ONLY one-byte message will be client ID messages.
                        if (client.assembledHeader == 1)
                        {
                            //char singleByte = (char)data[dataIndex - 1];
                            localClientID = (Int32)data[dataIndex - 1];
                            localClientIDValid = true;
                            if (clientConnectedDispatcher != null)
                            {
                                clientConnectedDispatcher.Dispatch(localClientID, ClientConnectedEvent.LOCAL_ID_SPECIFIER);
                            }
                        }
                        else
                        {
                            try
                            {
                                NetworkMessageData message = DeSerialize(client.fragmentedMessage);
                                HandleNetworkMessage(message, client, clientID);
                            }
                            catch (SerializationException ex)
                            {
                                Log("Error deserializing network datastream. Message dropped! " + ex.Message);
                            }
                        }

                        client.fragmentedMessageIndex = 0;
                        client.processingState = NetworkDataPocessingState.PROCESSING_HEADER;
                    }
                    break;
            }

            //not done processing the current data stream, so recurse
            if(dataIndex<dataSize)
                ReadNetworkDataStream(data, dataSize, dataIndex, client, clientID);       
        }

        private void BroadcastNetworkMessage(NetworkMessageData networkMessageData,NetworkClient source, Int32 SourceID)
        {
            networkMessageData.sourceClientID = SourceID;
            foreach (NetworkClient client in clientList)
            {
                if (client != null)
                {
                    if (client != source)
                    {
                        //byte[] data = Serialize(networkMessageData);

                        //TBD: may need blocking here
                        //Monitor.Enter(client);
                        //client.Write(data, 0, data.Length);
                        WriteNetworkMessage(networkMessageData, client.stream);
                        //Monitor.Exit(client);
                    }
                }
            }
        }

        private bool HandleNetworkMessage(NetworkMessageData networkMessageData, NetworkClient source, Int32 sourceID)
        {
            if (networkMessageData == null)
                return false;

            if (networkMessageData.BroadcastMessage && hosting)
            {
                BroadcastNetworkMessage(networkMessageData, source, sourceID);
            }

            if (NIUMessageDispatchers.ContainsKey(networkMessageData.MessageID))
            {
                NIUMessageDispatchers[networkMessageData.MessageID].Dispatch(networkMessageData);
            }

            return true;
        }

        public byte[] Serialize(NetworkMessageData message)
        {
            MemoryStream memoryStream = new MemoryStream();
            BinaryFormatter binaryFormatter = new BinaryFormatter(); 
            memoryStream.Seek(0, SeekOrigin.Begin);
            binaryFormatter.Serialize(memoryStream, message);
            memoryStream.Seek(0, SeekOrigin.Begin);
            //byte[] messageData = memoryStream.GetBuffer();
            byte[] messageData = memoryStream.ToArray();
            memoryStream.Close();

            return messageData;
        }

        public NetworkMessageData DeSerialize(byte[] data)
        {
            MemoryStream memoryStream = new MemoryStream();  
            memoryStream.Write(data, 0, data.Length);
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            memoryStream.Seek(0, SeekOrigin.Begin);
            NetworkMessageData message = (NetworkMessageData)binaryFormatter.Deserialize(memoryStream);
            memoryStream.Close();

            return message;
        }

        public bool Connect(IPEndPoint hostIP)
        {
            if (hosting)
            {
                Log("Call to Connect while hosting. Call ignored. Call StopHosting first.");
                return false;
            }     
            if (connectedToHost)
            {
                Log("Call to Connect while already connected. Call ignored. Call Disconnect first.");
                return false;
            }
            if (hostIP == null)
            {
                Log("Call to Connect with invalid IPEndPoint parameter. Call ignored.");
                return false;
            }
            try
            {
                
                TcpClient client = new TcpClient();
                client.Connect(hostIP);

                host.stream = client.GetStream();
                connectedToHost = true;

                Thread clientThread = new Thread(new ParameterizedThreadStart(NetworkMessageHandlerLoop));
                clientThread.Start(client);


            }
            catch (SocketException ex)
            {
                Log(String.Format("Connection to host {0} failed with message: {1}.",hostIP.Address.ToString(),ex.Message));
            }

            return true;
        }

        public void Disconnect()
        {
            CloseAllConnections();
        }

        private void CloseAllConnections()
        {
            localClientIDValid = false;
            localClientID = -1;


            if (hosting)
            {
                hosting = false;
                tcpListener.Stop();
                foreach (NetworkClient client in clientList)
                {
                    if (client != null)
                        if(client.stream != null)
                            client.stream.Close();
                }
            }
            if(connectedToHost)
            {    
                connectedToHost = false;
                if(host.stream!=null)
                    host.stream.Close(); 

            }

        }

        ~NIU()
        {
            CloseAllConnections();
        }
        
    }


    [Serializable()]
    public class NetworkMessageData
    {
        private Int32 messageID;
        internal bool broadcastMessage;
        internal Int32 sourceClientID;

        //force subclassers to specify the messageID at creation time
        public NetworkMessageData(Int32 messageID)
        {
            this.messageID = messageID;
        }

        //readonly
        public Int32 MessageID
        {
            get { return messageID; }
        }
        //readonly
        public bool BroadcastMessage
        {
            get { return broadcastMessage; }
        }
        //readonly
        public Int32 SourceClientID
        {
            get { return sourceClientID; }
        }
    }
}
